Skip to content

Work around intermittent SM120 FP8 gradient corruption in RTC cast-transpose#3215

Open
AlbertYang514 wants to merge 3 commits into
NVIDIA:mainfrom
AlbertYang514:fix/sm120-delayed-scaling-cast-transpose-ordering
Open

Work around intermittent SM120 FP8 gradient corruption in RTC cast-transpose#3215
AlbertYang514 wants to merge 3 commits into
NVIDIA:mainfrom
AlbertYang514:fix/sm120-delayed-scaling-cast-transpose-ordering

Conversation

@AlbertYang514

@AlbertYang514 AlbertYang514 commented Jul 15, 2026

Copy link
Copy Markdown

Summary

This change retains a device-scope __threadfence() in the optimized RTC cast_transpose kernel as a validated workaround for intermittent FP8 gradient corruption observed on SM120. The evidence supports describing this as a timing/codegen-sensitive workaround; the exact root cause is still unknown.

Observed issue

Under the affected checkpointed DelayedScaling workload, the forward loss remains normal, but backward can produce finite, extremely large gradients. The first affected layer and microbatch vary across runs. After the #3213 v2 checkpoint fix, the residual failure remains reproducible while FP8 ownership and update counts stay at the expected 1/1.

Change

The current code inserts __threadfence() on the optimized RTC cast_transpose path. This device-scope fence is retained because it has been stable in the tested reproductions. It does not change the public API or intended mathematical operation.

Investigation status

Nsight confirms that the optimized RTC cast-transpose kernel and the following dgrad GEMM execute on the same CUDA stream. Therefore, the original cross-kernel output-store/amax publication explanation is not supported by the execution model or the follow-up ablations. Seven tested fence placements were stable, including placements after the atomic operation, immediately before return, and under a thread-0-only condition. Replacing the fence with a system-scope atomic operation did not stabilize the no-fence variant, while a block-scope fence was also stable in the tested workload. Together, these results invalidate the earlier narrow publication-ordering explanation and instead point to a timing/codegen-sensitive effect. They do not identify the exact mechanism.

Validation

  • No-fence build: 5/10 failures in the fixed-seed sweep and 10/20 failures in the high-risk sweep; ownership/update counts remained 1/1.
  • Device-fence build: 0/10 failures in the fixed-seed sweep and 0/20 failures in the high-risk sweep; ownership/update counts remained 1/1.
  • Experimental block-fence build: 0/10 failures in the fixed-seed sweep and 0/20 failures in the high-risk sweep.
  • A 100-step numerical comparison produced 1,000 finite records; configurations L/M/N/O were stepwise identical in the non-triggering workload.
  • memcheck, racecheck, initcheck, and synccheck completed without a reported error; initcheck was also rerun without filtering. This is useful negative evidence, not proof that the kernel is free of races or memory-ordering defects.

Relationship to #3213

The residual failure remains reproducible after #3213 v2 restores FP8 ownership/update counts to 1/1. This separates the observed residual phenomenon from the known duplicate-update pathology, but does not prove that every underlying mechanism is fully independent.

Scope and limitations

The validation is specific to the tested SM120 hardware, CUDA 13 toolchain, and checkpointed DelayedScaling workload. The block-scope fence is experimental evidence only; this PR retains the device-scope fence. The root cause remains unresolved, and no claim is made for other architectures or workloads. The change does not alter public APIs or intended math.

Signed-off-by: AlbertYang514 <201034045+AlbertYang514@users.noreply.github.com>
Signed-off-by: AlbertYang514 <201034045+AlbertYang514@users.noreply.github.com>
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Jul 15, 2026
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a device-scope memory ordering race in the SM120 NVRTC-optimized cast_transpose kernel by inserting a single __threadfence() between the FP8 output stores and the relaxed atomicMaxFloat amax update, and adds a targeted regression test to validate the fix.

  • Kernel fix: __threadfence() is correctly placed after the last __syncthreads() in the transposed-output loop and before reduce_max + atomicMaxFloat, ensuring all global writes to output_c and output_t are device-visible before the amax atomic is published. All threads call the fence (since amax_ptr is a uniform parameter), and the subsequent warp-level reduction in reduce_max provides the necessary inter-thread ordering before thread 0 reaches the atomic.
  • Test: The new pytest creates a 14-layer SM120 stress network with activation checkpointing and DelayedScaling, runs three seeds × 3 steps × 64 microbatches, and checks per-microbatch corruption bounds plus a final gradient-norm range (0.001 < final_norm < 0.05). The statically compiled non-RTC fallback in cast_transpose.cu has the same structural pattern (global stores → __syncthreads()atomicMaxFloat) without a __threadfence(), so it carries a latent ordering risk on any path where the RTC kernel is not selected.

Confidence Score: 5/5

Safe to merge; the kernel change is a single targeted fence with no effect on computed values or public APIs, and the test validates the specific failure mode.

The production change is minimal — one __threadfence() call gated on an existing null-pointer check — and its placement (after all block-scope syncs, before the amax atomic) is correct. The validation and new regression test cover the specific failure scenario.

The statically compiled fallback in transformer_engine/common/transpose/cast_transpose.cu (lines 203-209) has the same pre-fence pattern and would benefit from the same treatment if the RTC path is ever unavailable on SM120.

Important Files Changed

Filename Overview
transformer_engine/common/transpose/rtc/cast_transpose.cu Inserts __threadfence() before the amax reduction/atomic to enforce device-scope ordering between all FP8 output stores and the DelayedScaling amax update; placement after the last __syncthreads() and before reduce_max is correct.
tests/pytorch/test_delayed_scaling_dgrad_ordering.py New SM120-only regression test using 14-layer stress network with activation checkpointing and DelayedScaling; checks per-microbatch corruption bounds and a final gradient-norm range; the final norm assertion bounds (0.001, 0.05) are appropriately widened from an earlier narrow version.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant K as cast_transpose kernel (SM120)
    participant GM as GPU Global Memory
    participant Atom as atomicMaxFloat (amax_ptr)
    participant DGEMM as dgrad GEMM kernel

    K->>GM: store output_c (rowwise FP8)
    K->>GM: store output_t (columnwise FP8, via shmem)
    Note over K: __syncthreads() — block-scope barrier only
    Note over K: __threadfence() — device-scope fence (NEW)
    K->>Atom: reduce_max + atomicMaxFloat(amax_ptr, amax)
    DGEMM->>GM: read FP8 outputs (guaranteed visible after fence)
    DGEMM->>Atom: read amax_ptr (for next-step scaling)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant K as cast_transpose kernel (SM120)
    participant GM as GPU Global Memory
    participant Atom as atomicMaxFloat (amax_ptr)
    participant DGEMM as dgrad GEMM kernel

    K->>GM: store output_c (rowwise FP8)
    K->>GM: store output_t (columnwise FP8, via shmem)
    Note over K: __syncthreads() — block-scope barrier only
    Note over K: __threadfence() — device-scope fence (NEW)
    K->>Atom: reduce_max + atomicMaxFloat(amax_ptr, amax)
    DGEMM->>GM: read FP8 outputs (guaranteed visible after fence)
    DGEMM->>Atom: read amax_ptr (for next-step scaling)
Loading

Reviews (2): Last reviewed commit: "test: relax SM120 dgrad norm bounds" | Re-trigger Greptile

Comment thread tests/pytorch/test_delayed_scaling_dgrad_ordering.py Outdated
Signed-off-by: AlbertYang514 <201034045+AlbertYang514@users.noreply.github.com>
@AlbertYang514

Copy link
Copy Markdown
Author

Hi @vthumbe1503 and @timmoon10, gentle ping when either of you has a chance.

This is a narrowly scoped SM120 RTC cast-transpose ordering fix: one device-scope fence before the DelayedScaling amax atomic, plus a targeted regression test.

It is currently mergeable, but has not received a human review yet. Could either of you please take a look, or point me to the current owner of this path?


// Reduce amax over block
if (amax_ptr != nullptr) {
// Order the global output stores before publishing the amax update.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually unclear why fp8 output store needs to happen before amax compute. Isnt the dgrad compute stream ordered anyway? Why would dgrad recieve incorrect fp8 values and threadfence helps it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vthumbe1503 — Your same-stream objection is correct, and the additional ablations do not support our original output-store/amax publication explanation. Seven fence placements were stable—including post-atomic, pre-return, and thread-0-only placements—while a system-scope atomic without the fence still failed, so I have rewritten the change as a timing/codegen-sensitive workaround. The current candidate retains the validated device-scope fence; the block-scope fence is documented only as experimental evidence. The exact root cause remains unknown, and the revised description now states that limitation explicitly.

@AlbertYang514 AlbertYang514 changed the title Fix SM120 FP8 output ordering before DelayedScaling amax update Work around intermittent SM120 FP8 gradient corruption in RTC cast-transpose Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants